/* License Notice:
**
** This program is free software: you can redistribute it and/or modify
**    it under the terms of the GNU General Public License as published by
**    the Free Software Foundation, either version 3 of the License, or
**    (at your option) any later version.
** This program is distributed in the hope that it will be useful,
**   but WITHOUT ANY WARRANTY; without even the implied warranty of
**   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
**   GNU General Public License for more details.
** You should have received a copy of the GNU General Public License
**   along with this program. If not, see <https://www.gnu.org/licenses/>.
**
** Originally this code was part of Breakout, now part of ArcadeFighter library.
** Breakout is free software: you can redistribute it and/or modify
**   it under the terms of the CC BY 4.0 license as published by
**   Creative Commons, either version 4 of the License, or (at your
**   option) any later version.
**
** Sean Barrett's STB library is published under the MIT License or Public Domain.
*/

/**
 * @file texture.hpp
 * @author TooOld2Rock'nRoll
 * @date 2022/Ago
 * @remark Requires an updated copy of Sean Barrett's (<i>nothings</i> on GitHub) stb_image.h
 * @brief Texture Class for the ArcadeFigher library.
 *
 * @see https://learnopengl.com/In-Practice/2D-Game/Breakout
 * @see https://github.com/nothings/stb
 *
 * @todo We should support simple texture blitting (stitching textures together)
 * @todo formalize the image_atlas.xml format and make a auto load method.
 * @todo We don't support tiles that are not squares, how TF do you map a blob??
 */
#ifndef _TEXTURE_H_
#define _TEXTURE_H_

/*---- Includes ----*/
#include <string>
#include <mutex>

#include "graphics/opengl.hpp"


/*---- Typedefs ----*/
/** @brief A more elegant name to keep track of Gl Texture IDs. */
typedef GLuint TextureID_t;


/*---- Macros ----*/
/** @brief Given a image size (x or y axle) and a pixel position, calculates the equivalent OpenGl coordinate. */
#define pixel2glmCoordinate(size, point) (point)/(float)(size)

/** @brief Given a image size (x or y axle) and a texture position, calculates the equivalent image pixel coordinate. */
#define glm2pixelCoordinate(size, point) (point)*(float)(size)


/*---- Classes ----*/
/**
 * @brief Class to create and handle the use of OpenGl textures.
 *
 * <i>A texture is an OpenGL Object that contains one or more images that all have the same image format.</i><br>
 *
 * This class main goal is to serve as an API for OpenGl's <i>GL_TEXTURE_2D</i> most common procedures.<br>
 *
 * You are encouraged to set your own options using the standard OpenGl procedures after loading
 *    the image file, just remember to bind() it first. The Textures section at LearnOpenGL.com is a recommended
 *    resource to understand this whole procedure in detail and its possibilities.<br>
 *
 * It also tries to facilitate same operations that are very common when developing games, like handling Tilesets
 *    (or Sprite Sheets or whatever you wanna call a big image file with smaller individual ones).<br>
 *
 * In case your tiles are uniform in size and contiguous, the <b>autoMap</b> methods in this class will do everything
 *    for you and your texture is ready to go; But, in very optimized projects, your tiles will fill only the bare minimum
 *    space, with little padding or not even be aligned to each other. For those cases, you will have to map each tile
 *    manually (or implement your own mapping method) as the following code:<br>
 * @code
 *    Texture *tx = new Texture ("myTilesetImage.png"); //create a texture with a texture map
 *
 *    tx->createTileMap (N);
 *    tx->mapTileByLimits (0, x1, y1, x2, y2);
 *    [...]
 *    tx->mapTileByLimits (N, n_x1, n_y1, n_x2, n_y2);
 * @endcode
 *
 * @remark If your textures are rendering upside down and you don't intend on changing OpenGl's orientation, set the
 *     flipOnLoad() method before loading any image, it's a static method that will set all images to be flipped on load.
 *
 * @see https://www.khronos.org/opengl/wiki/Texture
 * @see https://learnopengl.com/Getting-started/Textures
 */
class Texture
{
    public:
        /** @brief The UV coordinates to the tile limits in the image file in OpenGl coordinate system. */
        struct tile_uv
        {
            unsigned u_width = 0; ///<The tile original <b>width</b> in pixels.
            unsigned u_height = 0; ///<The tile original <b>height</b> in pixels.

            unsigned u_x1 = 0;///< Left coordinate in pixels.
            unsigned u_y1 = 0;///< Top coordinate in pixels.
            unsigned u_x2 = 0;///< Right coordinate in pixels.
            unsigned u_y2 = 0;///< Bottom coordinate in pixels.

            float f_x1 = 0;///< Left coordinate in texture position.
            float f_y1 = 0;///< Top coordinate in texture position.
            float f_x2 = 0;///< Right coordinate in texture position.
            float f_y2 = 0;///< Bottom coordinate in texture position.
        };//end tile_uv


    private:
        //keeps track of the last texture to be bind so it doesn't have to do it again over multiple calls.
        static volatile TextureID_t last_texture;

        //holds the ID of the texture object, used for all texture operations to reference to this particular texture
        TextureID_t ID = 0;
        std::atomic<bool> b_loaded = false;

        //How many tiles are being mapped
        unsigned u_tile_count = 0;
        //texture tile mapping
        Texture::tile_uv *pa_texture_map = nullptr;

    protected:
        bool b_mono_black_as_alpha = false; ///<In case a monocrome texture requires to use black as alpha.

        unsigned u_width = 0; ///<The image <b>width</b> in pixels.
        unsigned u_height = 0; ///<The image <b>height</b> in pixels.

        //for blit operation
        static FrameBuffObj_t FBO[2]; ///<The FrameBuffers to read/write.
        static std::mutex mtx_blit_semaphore; ///<Be sure only one blit operation is running.


    public:
        Texture ();
        Texture (const char *file_path);

        virtual ~Texture ();

        /** @brief Textures will be considered equal if their IDs are the same. */
        bool operator == (const Texture &t) const { return (this->ID == t.ID); }

        void bind ();

        /** @brief Gets the Gl texture identity. */
        TextureID_t getID () const { return this->ID; }

        /** @brief Gets the image <b>width</b> in pixels. */
        unsigned getWidth () const { return this->u_width; };
        /** @brief Gets the image <b>height</b> in pixels. */
        unsigned getHeight () const { return this->u_height; };

        static void flipOnLoad (bool flip);
        /** @brief In case this texture requires to use black as alpha for transparency when there is no alpha channel.*/
        void useMonocromeAsAlpha (bool black_for_alpha) { this->b_mono_black_as_alpha = black_for_alpha; }
        virtual void load (const char *file_path);
        void load (unsigned char *data, unsigned width, unsigned height, unsigned channels);
        /** @brief Returns <b>true</b> if the Texture was already loaded with an image file/buffer. */
        bool loaded () { return this->b_loaded; }

        /*** Helper methods to map textures ***/
        const Texture::tile_uv& createTileMap (unsigned tile_count);
        const Texture::tile_uv& createTileMap (unsigned columns, unsigned rows, const unsigned (&padding)[4] = { });

        /** @brief Gets the size of the tile map array. */
        unsigned getTilesCount () const { return this->u_tile_count;};
        /** @brief Gets the tile map array for this texture.
         * @return <b>nullptr</b> if Texture has no tile map, array of tiles otherwise.
         */
        const Texture::tile_uv* getTileMap () const { return this->pa_texture_map; }
        const Texture::tile_uv& getTileLimits (unsigned index) const;

        void autoMapTiles (unsigned columns, unsigned rows, const unsigned (&padding)[4] = { });
        void autoMapTiles (unsigned x_start, unsigned y_start, unsigned tile_w, unsigned tile_h, const unsigned (&padding)[4] = { });
        void mapTileByLimits (unsigned tile_index, unsigned x1, unsigned y1, unsigned x2, unsigned y2, const unsigned (&padding)[4] = { });
        void mapTileBySize (unsigned tile_index, unsigned x, unsigned y, unsigned w, unsigned h, const unsigned (&padding)[4] = { });

        static void blitSet (TextureID_t source_id, TextureID_t target_id);
        static void blit (int src_x, int src_y, int dst_x, int dst_y, unsigned w, unsigned h, GLbitfield mask, GLenum filter);
        static void blitEnd ();
};//END Texture
#endif //_TEXTURE_H_

